﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using TwinCAT.Ads;
using MTS.IO.Channel;
using MTS.IO.Address;

namespace MTS.IO.Module
{
    public sealed class ECModule : IModule
    {
        #region Channels alocated memory

        // connection object
        private readonly TcAdsClient client = new TcAdsClient();
        // all channels
        private readonly Dictionary<string, ChannelBase<ECAddress>> channels = new Dictionary<string, ChannelBase<ECAddress>>();

        private BinaryWriter iWriter;      // input writer
        private AdsStream iReadStream;     // input read stream
        private int iReadStreamOffset;
        private const int readCommand = 0xF080;     // constant that is entered to method call when reading

        private BinaryWriter oWriter;   // output writer
        private AdsStream oReadStream;  // output read stream
        private int oWriterOffset;      // 
        private const int writeCommand = 0xF081;    // constant that is entered to method call when writing

        #endregion

        /// <summary>
        /// (Get/Set) Name of task in TwinCAT IO Server. This name is necessary for variable handles
        /// </summary>
        public string TaskName { get; set; }

        #region IModule Members

        private readonly char[] csvSep = { ';' };
        private readonly char[] floatSep = { '.' };
        private const string boolString = "bool";

        private const string inputString = "Input";
        private const string outputString = "Output";

        /// <summary>
        /// Load configuration of channels form file. In case of EtherCAT (Beckhoff) implementation
        /// this file should be .CSV file generated by TwinCAT System Manager. When configuration is being
        /// loaded, must not be Listening yet.
        /// </summary>
        /// <param name="filename">Path to file where configuration of channels is stored</param>
        public void LoadConfiguration(string filename)
        {
            // When loading channels: all of them are added to inputs and they are added to this collection
            // Some of these channels are also outputs - to write them when channels are update, add them to
            // outputs collection
            // CSV format:
            // Name; ;Type;Size;>Address;In/Out;User ID;Linked to
            // Example: HeatingFoilCurrent;X;INT;2.0;0.0;Input;0;Data In . Channel 1 . Term 2 (KL3152) . Box 1 (BK1120) . Device 1 (EtherCAT) . I/O Devices

            // parse TwinCAT configuration file
            ChannelBase<ECAddress> channel;
            string tmp;
            int size;
            string[] items;
            StreamReader reader = new StreamReader(filename);

            // read file format - columns may be unordered
            items = reader.ReadLine().Split(csvSep, StringSplitOptions.None);
            int nameIndex = Array.IndexOf<string>(items, "Name");
            int typeIndex = Array.IndexOf<string>(items, "Type");
            int addressIndex = Array.IndexOf<string>(items, ">Address");
            int sizeIndex = Array.IndexOf<string>(items, "Size");
            int inOutIndex = Array.IndexOf<string>(items, "In/Out");

            while (!reader.EndOfStream)
            {
                items = reader.ReadLine().Split(csvSep, StringSplitOptions.None);

                tmp = items[typeIndex].ToLower();   // type of variable
                if (tmp == boolString)              // BOOL is a digital channel
                {
                    if (items[inOutIndex] == inputString)
                        channel = new DigitalInput<ECAddress>();
                    else if (items[inOutIndex] == outputString)
                        channel = new DigitalOutput<ECAddress>();
                    else throw new ChannelException(string.Format(Resource.UnkownChannelIOTypeMsg, items[inOutIndex]));
                    size = sizeof(bool);    // even if size is one bit - in program we use it as one byte (bool)
                }
                else                        // not bool (some kind of int) is an analog channel
                {
                    // read channel type: input/output
                    if (items[inOutIndex] == inputString)
                        channel = new AnalogInput<ECAddress>();
                    else if (items[inOutIndex] == outputString)
                        channel = new AnalogOutput<ECAddress>();
                    else throw new ChannelException(string.Format(Resource.UnkownChannelIOTypeMsg, items[inOutIndex]));
                    // parse channel size                    
                    size = (int)double.Parse(items[sizeIndex], System.Globalization.CultureInfo.InvariantCulture);
                }
                channel.Id = items[nameIndex];
                channel.Name = items[nameIndex];
                channel.Size = size;

                // create a particular type of address for this kind of channel
                ECAddress addr = new ECAddress();
                // Full name in format: TaskName.Inputs.VariableName
                // notice the "s" at the end of "Input" or "Output" string
                addr.FullName = string.Format("{0}.{1}s.{2}", TaskName, items[inOutIndex], items[nameIndex]);
                // initialize variable "address"
                addr.IndexGroup = (int)AdsReservedIndexGroups.SymbolValueByHandle;

                // set channel address
                channel.Address = addr;

                channels.Add(channel.Id, channel);
            }

            reader.Close();
        }

        private void beforeConnect()
        {
            
        }
        /// <summary>
        /// Establish a connection to ADS device
        /// </summary>
        /// <exception cref="MTS.IO.Module.ConnectionException">Connection could not be established</exception>
        public void Connect()
        {
            beforeConnect();
            // notice that we are connecting to local server which by the way handle any communication 
            // with remote side
            if (client.IsConnected)
                return;
            try
            {
                client.Connect(AmsNetId.Local, 301);    // ??? fuck - write me an email
            }
            catch (Exception ex)
            {   // establishing connection failed
                throw new ConnectionException(Resource.ConnectionFailedMsg, ex) { ProtocolName = this.ProtocolName };
            }
            afterConnect();
        }
        private void afterConnect()
        {   // see that we are calling a method of the client - connection must be established already
            foreach (ChannelBase<ECAddress> channel in this.Inputs)
            {
                try
                {   // try to initialize each channel address
                    channel.Address.IndexOffset = client.CreateVariableHandle(channel.Address.FullName);
                }
                catch (Exception ex)
                {   // variable handle of some address could not be created
                    throw new AddressException(string.Format(Resource.VarNotFoundMsg, ProtocolName, channel.Address), ex)
                    {
                        ChannelName = channel.Name,
                        ProtocolName = this.ProtocolName
                    };
                }
            }
            // at this time all addresses must be already initialized
            alocateChannels();  // allocate memory for reading a writing channels
        }

        /// <summary>
        /// Prepare (initialize) channels for reading and writing. This method is called after new instance of module is
        /// created and before module is connected
        /// </summary>
        public void Initialize()
        {
            
        }

        /// <summary>
        /// Write all output and read all input channels (in this order)
        /// </summary>
        public void Update()
        {
            UpdateOutputs();    // this order must be fixed
            UpdateInputs();
        }
        /// <summary>
        /// Read all inputs and outputs channels
        /// </summary>
        public void UpdateInputs()
        {
            int count = this.Inputs.Count();
            // do not read if there are no inputs
            if (count == 0) return;
            // jump at the beginning of the read stream - read data are going to be written here
            iReadStream.Seek(0, SeekOrigin.Begin);
            // read data from hardware to read stream
            client.ReadWrite(readCommand, count, iReadStream, (AdsStream)iWriter.BaseStream);
            // jump at the beginning of the data (skip error codes)
            iReadStream.Seek(iReadStreamOffset, SeekOrigin.Begin);

            // remove this
            BinaryReader reader = new BinaryReader(iReadStream);

            // read values from stream and write to input channels
            foreach (var channel in Inputs)
            {   // check error codes and throw exception if some error occurs ...
                channel.ValueBytes = reader.ReadBytes(channel.Size);
            }
        }
        /// <summary>
        /// Write all outputs channels
        /// </summary>
        public void UpdateOutputs()
        {
            int count = Outputs.Count();
            // do not write if there are no outputs
            if (count == 0) return;
            // seek to position behind info data (IndexGroup, IndexOffset and Size), bytes before never change
            oWriter.Seek(oWriterOffset, SeekOrigin.Begin);
            // write channels values
            foreach (var channel in Outputs)
                oWriter.Write(channel.ValueBytes);
            // return at previous position where outputs start
            oWriter.Seek(oWriterOffset, SeekOrigin.Begin);
            // write values from stream to hardware
            client.ReadWrite(writeCommand, count, oReadStream, (AdsStream)oWriter.BaseStream);
            // check error codes and throw exception if some error occurs
        }

        /// <summary>
        /// Close connection between local computer and some hardware component
        /// </summary>
        public void Disconnect()
        {
            // delete variable handles from TwinCAT IO Server
            foreach (var channel in channels.Values)
                client.DeleteVariableHandle(channel.Address.IndexOffset);
        }

        /// <summary>
        /// (Get) Value indicating that this module is Listening to remote hardware
        /// </summary>
        public bool IsConnected
        {
            get { return (client != null) ? client.IsConnected : false; }
        }

        /// <summary>
        /// (Get) Name of this module communication protocol
        /// </summary>
        public string ProtocolName { get; private set; }

        /// <summary>
        /// Get an instance of particular channel identified by its name. Return null if there is no such a channel.
        /// In case of Beckhoff (EtherCAT) IModule implementation this is TwinCAT IO Server variable name.
        /// </summary>
        /// <param name="id">Unique identifier of required channel</param>
        /// <exception cref="ChannelException">Channel identified by its name does not exists in current
        /// module</exception>
        public IChannel GetChannel(string id)
        {
            try
            {
                return channels[id];
            }
            catch (Exception ex)
            {
                throw new ChannelException(Resource.ChannelNotFoundMsg, ex) { ChannelName = id, ProtocolName = this.ProtocolName };
            }
        }
        /// <summary>
        /// Enumerate collection of input channels. All outputs are inputs as well
        /// </summary>
        public IEnumerable<IChannel> Inputs
        {
            get
            {
                foreach (var channel in channels.Values)
                    if (channel is IAnalogInput || channel is IDigitalInput)
                        yield return channel;
            }
        }
        /// <summary>
        /// Enumerate collection of output channels
        /// </summary>
        public IEnumerable<IChannel> Outputs
        {
            get
            {
                foreach (var channel in channels.Values)
                    if (channel is IDigitalOutput || channel is IAnalogOutput)
                        yield return channel;
            }
        }
        /// <summary>
        /// Enumerate collection of channels of given type. For example all analog input channels
        /// </summary>
        /// <typeparam name="TChannel">Type of channel to enumerate</typeparam>
        /// <returns>Collection of channels of particular type</returns>
        public IEnumerable<TChannel> GetChannels<TChannel>() where TChannel : IChannel
        {
            foreach (var channel in Inputs)
                if (channel is TChannel)
                    yield return (TChannel)channel;
        }

        public TChannel GetChannel<TChannel>(string id) where TChannel : IChannel
        {
            try
            {
                TChannel channel = (TChannel)(IChannel)channels[id];
                if (channel != null)
                    return channel;
            }
            catch (Exception ex)
            {
                throw new ChannelException(Resource.ChannelNotFoundMsg, ex) { ChannelName = id, ProtocolName = this.ProtocolName };
            }
            throw new ChannelException(string.Format(Resource.ChannelOfTypeNotFoundMsg, typeof(TChannel), id)) { ChannelName = id, ProtocolName = this.ProtocolName };
        }

        #region IEnumerable Members

        public System.Collections.IEnumerator GetEnumerator()
        {
            return channels.GetEnumerator();
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {   // disconnect
            Disconnect();
            // release resources allocated by TwinCAT IO Server
            client.Dispose();
            iReadStream.Dispose();
            oReadStream.Dispose();
            iWriter.Dispose();
            oWriter.Dispose();
        }

        #endregion

        #endregion

        #region Channel Handlig

        /// <summary>
        /// Allocate and initialize memory necessary for channel handling. This method is called only once
        /// and then initialized memory is reused.
        /// </summary>
        private void alocateChannels()
        {
            // 1. allocate memory for input channels
            int count = Inputs.Count();  // number of items to read
            // for each variable read from hardware 4 more bytes for error status are necessary
            int readLength = count * 4;     // we are going to skip these bytes when reading inputs
            // position in the stream where value for reading begins, before are only error codes
            iReadStreamOffset = readLength;
            // Information about reading variables are stored in a stream. We put this values to stream
            // through BinaryWriter. This is space necessary for additional info about reading variable:
            // IndexGroup, IndexOffset, Size - 4B for each item
            int writeLength = count * 12;

            iWriter = new BinaryWriter(new AdsStream(writeLength));
            foreach (ChannelBase<ECAddress> channel in Inputs)
            {
                iWriter.Write(channel.Address.IndexGroup);
                iWriter.Write(channel.Address.IndexOffset);
                iWriter.Write(channel.Size);
                readLength += channel.Size; // count size of read memory (+error codes)
            }
            // to this stream data are going to be read - size is sum of all variable sizes + error codes
            iReadStream = new AdsStream(readLength);


            // 2. allocate memory for writing channels
            count = Outputs.Count();  // number of items to write
            // For each variable wrote an error code (4B size) is returned. This is size of memory for all error codes
            readLength = count * 4;
            // Information about writing variables are stored in a stream. We put this values to stream
            // through BinaryWriter. This is space necessary for additional info about writing variable:
            // IndexGroup, IndexOffset, Size - 4B for each item
            writeLength = count * 12;       // but that is not all - add space for values of writing channels
            oWriterOffset = writeLength;    // position in the stream where values for writing begins, before are only info data
            foreach (var channel in Outputs)// add memory for each variable that is going to be written
                writeLength += channel.Size;

            // create BinaryWriter and write info data about every variable (channel)
            oWriter = new BinaryWriter(new AdsStream(writeLength));
            foreach (ChannelBase<ECAddress> channel in Outputs)
            {
                oWriter.Write(channel.Address.IndexGroup);    // notice that data are wrote at the end
                oWriter.Write(channel.Address.IndexOffset);
                oWriter.Write(channel.Size);
            }
            // when writing add data at the end of this writer - oWriterOffset points here

            // to this stream error codes will be written
            oReadStream = new AdsStream(readLength);
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Create a new instance of EtherCAT module
        /// </summary>
        /// <param name="protocolName">Name of ethercat protocol</param>
        /// <param name="taskName">Name of task in TwinCAT IO Server. This is necessary for variables
        /// handles</param>
        public ECModule(string protocolName, string taskName)
        {
            ProtocolName = protocolName;
            TaskName = taskName;            // this is necessary for variable handles
        }

        #endregion
    }
}
